Skip to content

Add SDK utility for off-chain operations#12

Open
yinyiqian1 wants to merge 20 commits intoXRPLF:mainfrom
yinyiqian1:mpt-utility
Open

Add SDK utility for off-chain operations#12
yinyiqian1 wants to merge 20 commits intoXRPLF:mainfrom
yinyiqian1:mpt-utility

Conversation

@yinyiqian1
Copy link

@yinyiqian1 yinyiqian1 commented Feb 17, 2026

All the off-chain operations should go through this layer.
For example:

  • Key generation
  • Blinding factor generation
  • Encryption
  • Decryption
  • Context hash generation for each tx
  • Proof generation for each tx

The interface can be found in:

  • include/utility/mpt_utility.h

The implementation details:

  • src/utility/mpt_utility.cpp

For details on how to use these functions, the test cases contain the demos on how to generate proof for each transaction:

  • tests/test_mpt_utility.cpp

It acts as the producer of the required off-chain values when submitting requests to rippled, providing a single source of truth for all off-chain operations so everyone can consistently prepare their requests.

This can be consumed by:

  • Rippled's test framework
  • QA framework
  • Libs
  • Other users

@yinyiqian1 yinyiqian1 changed the title Support an utility layer that generates proof for all the tx Support an utility layer that do all the off-chain operations Feb 17, 2026
Copy link
Collaborator

@mathbunnyru mathbunnyru left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that mpt_utility.h is a C++ only header, but some things are implemented in C, some in C++.
I would prefer sticking with one and not mixing them.

Copy link
Contributor

@mrtcnk mrtcnk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments mostly as reference points. Nothing blocking from my side. Happy to iterate further in follow-up PRs as the utility layer evolves.


secp256k1_context*
mpt_secp256k1_context();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small note: can we ensure the TransactionContextID construction here matches the spec
H(TxType ∥ Account ∥ MPTokenIssuanceID ∥ SequenceOrTicket ∥ TxSpecific)
where:

  • Send / ConvertBack: TxSpecific = Receiver ∥ CBSVersion
  • Convert: TxSpecific = Account ∥ 0

Copy link
Author

@yinyiqian1 yinyiqian1 Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context hash is adapting to the rippled's implementation hash. We'll change on both sides if anything requires modification.

The current hashing is like:
We have a common hash matches TxType ∥ Account ∥ MPTokenIssuanceID ∥ SequenceOrTicket, see the function mpt_add_common_zkp_fields.

Convert: TxSpecific = amount. (see mpt_get_convert_context_hash). because we are trying to add all the inputs into the hash function. (account already included in the common hash)
ConvertBack : TxSpecific = amount | CBSVersion. (account already included in the common hash)
Send: TxSpecific = receiver | CBSVersion.
Clawback: TxSpecific = amount | holder

The difference with the doc:

  • introduced for convert/convertback/clawback.
  • some TxSpecific fields omit the Account field, since it is already included in the common hash section.

What we will add according to what you suggested in the doc:

  • We need to pass in the ticket number if it is a ticket, although the interface won't change.
  • We'll add version 0 for convert: TxSpecific = amount | 0

Please suggest if this looks good to you or any other modifications needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed breakdown! The common hash structure aligns well with the spec. From the spec side, I’d prefer that we keep the TransactionContextID definition. The cryptographic intent there is that TxSpecific binds the transaction context and state freshness (identity||version), rather than public payload inputs like the plaintext amount. For example, in ConfidentialMPTConvert, the spec intentionally defines TxSpecific := Account||0 to maintain a uniform semantic structure across transaction types, where 0 explicitly indicates that no CBSVersion is involved. The amount is already bound inside the Fiat–Shamir transcript (mG) during challenge computation, so it does not need to be part of TransactionContextID.

To keep the transcript definition consistent with the spec, my preference for the semantic shape of TxSpecific would be:

ConfidentialMPTSend: receiver||CBSVersion
ConfidentialMPTConvertBack: receiver (=Account)||CBSVersion
ConfidentialMPTConvert: account||0
ConfidentialMPTClawback: holder||0

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces a utility layer for off-chain cryptographic operations related to Multi-Purpose Token (MPT) confidential transactions. The layer provides a unified API for encryption, proof generation, and context hash computation, serving as the single source of truth for these operations across rippled's test framework, QA framework, and client libraries.

Changes:

  • Added mpt_utility.h header defining the utility API with functions for keypair generation, encryption/decryption, proof generation, and context hash computation
  • Implemented mpt_utility.cpp with cryptographic primitives including ElGamal encryption, Pedersen commitments, and zero-knowledge proof generation
  • Added comprehensive test suite test_mpt_utility.cpp covering encryption, convert, send, convert-back, and clawback operations

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 20 comments.

Show a summary per file
File Description
include/utility/mpt_utility.h Header file defining the public API for MPT off-chain operations with constants, types, and function declarations
src/utility/mpt_utility.cpp Implementation of cryptographic operations including encryption, proof generation, and context hashing with platform-specific endianness handling
tests/test_mpt_utility.cpp Test suite verifying encryption roundtrips and proof generation/verification for all confidential transaction types
CMakeLists.txt Build configuration update to include the new utility source file
tests/CMakeLists.txt Test configuration update to add the new utility test with required dependencies

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -0,0 +1,642 @@
#include <arpa/inet.h>
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The include of arpa/inet.h is not portable to Windows. While Windows is handled with the _WIN32 defines for byte order operations below, the header will fail to compile on Windows. Consider making this include conditional with #ifndef _WIN32 or using winsock2.h for Windows instead.

Copilot uses AI. Check for mistakes.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @yinyiqian1 , can this issue be fixed? I had to create a patch on our end to handle Windows. Probably something like this should work:

#ifdef _WIN32
    #include <winsock2.h>
#else
    #include <arpa/inet.h>
#endif

if (!ctx)
return -1;

std::vector<secp256k1_pubkey> r(n_recipients);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation: The function does not validate that n_recipients is greater than zero. If n_recipients is 0, the code will access pk[0].data at lines 580 and 587, which is undefined behavior. Add a check to ensure n_recipients > 0 at the beginning of the function.

Copilot uses AI. Check for mistakes.
@yinyiqian1 yinyiqian1 changed the title Support an utility layer that do all the off-chain operations Add SDK utility for off-chain operations Feb 26, 2026
* @brief Represents a recipient in a Confidential Send transaction.
* Contains a pubkey and an encrypted amount.
*/
struct mpt_confidential_recipient
Copy link

@Patel-Raj11 Patel-Raj11 Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yinyiqian1 Hi, Would it make sense to call this struct as mpt_confidential_participant or something else that does not imply that it is a recipient of the confidential amount? As other then the receiver all other are the participants of the send transaction?

Copy link
Author

@yinyiqian1 yinyiqian1 Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Patel-Raj11 Thank you. Yes, it makes sense. How about mpt_confidential_party, which is shorter, but similar to participant?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mpt_confidential_party sounds good to me.

if (secp256k1_ec_pubkey_parse(ctx, &pk[i], rec.pubkey, kMPT_PUBKEY_SIZE) != 1)
return -1;

sr.insert(sr.end(), tx_blinding_factor, tx_blinding_factor + kMPT_BLINDING_FACTOR_SIZE);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if each participant/recipient has different blinding factor, the proof generation and verification both works. Since there is no BlindingFactor field for ConfidentialMPTSend transaction type, different blinding factor's can be used by each participant to encrypt the same amount.
Could you confirm this on your end once and if it's true then should we append multiple blinding factors here?

Copy link
Author

@yinyiqian1 yinyiqian1 Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. We should use the same blinding factor for encrypting the participant/recipient. The blinding factor is generated by the sender off-chain. It is a value that zkproof needs to verify, it should not be part of the request.
If we use different blinding factors for the ElGamal ciphertexts, the Equality Proof inside the ZK-Proof would fail.

We can use another set of blinding factor for pedersen commitment. But for the ElGamal ciphertexts, we must use the same blinding factor for issuer,auditor and sender.

Btw, in ConfidentialMPTConvert, it is a public amount, we can put the blinding factor in the request. We are not trying to prove the knowledge of the randomness in that case. We are proving the knowledge of private key in that case, using schnorr proof.

Key difference:

  • convert: prove the knowledge of identity (private key)
  • send (equality proof part): prove the knowledge of randomness (blinding factor) and the actual value we are encrypting.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use different blinding factors for the ElGamal ciphertexts, the Equality Proof inside the ZK-Proof would fail.

I have an integration test that runs on this commit and that test uses different blinding factors for sender, receiver and issuer. The proof that gets submitted to rippled still gets verified successfully. test_valid_multi_proof in mpt-crypto also verifies with different blinding factor. So wanted to know if something changed recently or it's some logic in rippled verification part that requires same blinding factor to be used for encryption for Send transaction?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Patel-Raj11 Sorry for the confusion. We're going to have an optimization that is forcing the shared r. This was newly supported from the proof_same_plaintext_multi_shared_r.c. By that time, different blinding factor will fail.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, sounds good. I'll change my implementation now to use same blinding factor, but there will be change in the utility functions to use secp256k1_mpt_prove_equality_shared_r right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That is correct

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I'll update the Rippled's c++ code first, and then make the changes in this SDK lib.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants